CMake 是什么
什么是 CMake
CMake 是一个跨平台的自动化构建系统,它使用简洁的配置文件(CMakeLists.txt)来描述项目构建过程。CMake 可以生成适用于不同操作系统和开发环境的原生构建配置,包括 Visual Studio 的解决方案文件 (.sln)。
即 CMake是 用来生成 Makefile 的一个工具:读入所有源文件之后,自动生成 Makefile

-
CMakeLists.txt: 这个文件是 CMake 项目的核心,定义了项目名称、包含的源文件、依赖关系以及其他构建参数。通过编写 CMakeLists.txt 文件,你可以控制编译过程并使项目在不同的平台上更容易构建。
-
集成到 Visual Studio: 自 Visual Studio 2017 起,Visual Studio 已经提供了对 CMake 项目的原生支持。你可以直接打开 CMakeLists.txt 文件作为项目来开始你的工作,无需额外生成 Visual Studio 解决方案文件。
创建 CMake 项目


- src : 源码工程目录
- ext : 第三方依赖库文件与头文件
- CMakeLists.txt : CMake 构建配置文件
简单案例
源文件编写 : src/main.cpp
#include <iostream>
int main()
{
std::cout << "Hello CMake." << std::endl;
return 0;
}
编写 CMake 配置文件 CMakeLists.txt
# CMakeList.txt: StudyCMake 的 CMake 项目,在此处包括源代码并定义
# 项目特定的逻辑。
# cmake 最低版本需求
cmake_minimum_required (VERSION 3.8)
# 工程名称
project ("StudyCMake")
# 将源代码添加到此项目的可执行文件。
add_executable (StudyCMake "src/main.cpp" "src/StudyCMake.h")
# TODO: 如有需要,请添加测试并安装目标。
cmake 命令便按照 CMakeLists 配置文件运行构建 Makefile 文件
$ mkdir build
$ cd build/
$ cmake ..
为了不让编译产生的中间文件污染我们的工程,我们可以创建一个 build 目录进入执行 cmake 构建工具。 如果没有错误, 执行成功后会在 build 目录下产生 Makefile 文件。
-rw-r--r-- 1 root root 13591 Jul 20 12:09 CMakeCache.txt
drwxr-xr-x 14 root root 448 Jul 20 12:09 CMakeFiles
-rw-r--r-- 1 root root 5034 Jul 20 12:09 Makefile
-rw-r--r-- 1 root root 1508 Jul 20 12:09 cmake_install.cmake
-rwxr-xr-x 1 root root 9104 Jul 20 12:09 cmake_study
然后我们执行 make 命令就可以编译我们的项目了。
$ ./cmake_study
Hello, World!
定义一个可执行目标
add_executable 是 CMake 中的一个命令,用于定义一个可执行目标(即最终生成的可执行文件)以及它所依赖的源文件。当 CMake 配置你的项目时,add_executable 告诉 CMake 需要编译哪些文件以及如何将它们链接成一个可执行文件。
基本语法
add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] [source1] [source2 ...])
<name>: 目标的名称,也是生成的可执行文件的名称。[WIN32],[MACOSX_BUNDLE]: 特定平台的选项,例如,WIN32用于创建一个没有控制台窗口的 Windows 应用程序。[EXCLUDE_FROM_ALL]: 如果设置了这个选项,这个目标不会被默认构建,除非有其他的目标依赖它,或者显式地构建了这个目标。[source1] [source2 ...]: 列出构成这个可执行文件的源文件。
假设你有一个项目,包含 main.cpp 和 hello.cpp 两个源文件,你想要生成一个名为 hello_world 的可执行文件:
add_executable(hello_world main.cpp hello.cpp)
这行命令告诉 CMake 创建一个名为 hello_world 的可执行目标,它由 main.cpp 和 hello.cpp 这两个源文件编译而成。
在一些更复杂的项目中,源文件可能会非常多,手动列出所有源文件可能会很不方便。CMake 提供了一些方法来自动收集源文件列表,例如使用 file(GLOB ...) 命令:
file(GLOB SOURCES "*.cpp")
add_executable(hello_world ${SOURCES})
这个例子中,file(GLOB ...) 命令搜索当前目录下的所有 .cpp 文件,并将它们的列表存储在变量 SOURCES 中。然后,这个变量被用作 add_executable 命令的参数,来定义可执行目标。
虽然使用 file(GLOB ...) 方式可以方便地自动收集源文件,但这种方法有一个缺点:如果后来添加或删除了源文件,CMake 不会自动知道需要重新运行来更新项目配置。因此,对于随时间变化的源文件列表,你可能需要手动重新运行 CMake。因此,在很多情况下,推荐显式地列出所有源文件,以确保项目的可追踪性和可维护性。
常用操作
宏定义与函数
在 CMake 中,宏(macro)和函数(function)允许你封装和复用代码逻辑。虽然它们在语法上非常相似,但在作用域和命名空间方面有一些关键的区别。
宏类似于传统编程语言中的文本替换工具。当你调用一个宏时,它的内容(代码块)会在调用点展开。宏内的变量不会创建新的作用域,即它会使用调用它的作用域中的变量。
定义宏:
macro(print_message message)
message(STATUS "Printing message: ${message}")
endmacro(print_message)
使用宏:
print_message("Hello from macro!")
在这个例子中,print_message 宏会打印一条状态消息。由于宏不会创建新的变量作用域,message 变量会直接访问宏调用时传入的值。
函数(Function)
函数与宏非常相似,但它会为其内部的变量和参数创建一个新的作用域。这意味着在函数内部定义或修改的变量不 会影响到外部作用域,除非使用 PARENT_SCOPE 明确指定。
定义函数:
function(print_message message)
message(STATUS "Printing message: ${message}")
endfunction(print_message)
使用函数:
print_message("Hello from function!")
这个例子中的 print_message 函数的行为看起来与上面的宏类似,但是因为它会创建自己的作用域,所以在函数内部对 message 变量的任何修改都不会影响到外部变量。
宏和函数的区别
关键的区别在于作用域。使用宏时,代码直接插入到调用位置,共享同一作用域;而函数则在自己的作用域内执行,不会污染外部环境。选择宏还是函数取决于你是否需要隔离变量和参数。
示例:循环添加编译定义
假设你想为多个目标添加相同的编译定义,可以定义一个函数来复用这段逻辑:
# 定义函数
function(add_compile_defs)
foreach(target IN LISTS ARGN)
target_compile_definitions(${target} PRIVATE
USE_CUSTOM_DEFINITION=1)
endforeach()
endfunction()
# 创建目标
add_executable(app1 main1.cpp)
add_executable(app2 main2.cpp)
# 使用函数为多个目标添加编译定义
add_compile_defs(app1 app2)
在这个例子中,add_compile_defs 函数接受任意数量的目标,并为每个目标添加了 USE_CUSTOM_DEFINITION=1 的编译定义。这展示了如何通过函数简化和复用构建脚本中的逻辑。
CMake 实际使用例子
首先相关问题都可以参考 官方文档
下面这个例子是一个 OCR 项目的 CMakeLists.txt 文件,以此为例子来说明 CMakeLists.txt 文件的编写
# 根据不同的操作系统(Windows、Apple macOS、其他 UNIX 系统),脚本指定了不同的最低 CMake 版本要求。这是因为不同平台可能需要不同版本的 CMake 功能支持。
if (WIN32)
cmake_minimum_required(VERSION 3.12)
elseif (APPLE)
cmake_minimum_required(VERSION 3.17)
elseif (UNIX)
cmake_minimum_required(VERSION 3.17)
endif ()
# 项目声明: project(RapidOcrOnnx) 声明了这个项目的名称是 RapidOcrOnnx。
project(RapidOcrOnnx)
# 输出类型和 ONNX 运行时选项: 脚本定义了两个关键的构建选项:
# OCR_OUTPUT 和 OCR_ONNX,分别用于决定输出类型(比如二进制文件、共享库或 JNI 库)和使用的 ONNX 运行时类型(CPU 或 CUDA)。
# 如果这些变量没有预先定义,脚本会为它们设置默认值,并显示相应的状态消息。
# Output BIN JNI CLIB
if (NOT DEFINED OCR_OUTPUT)
set(OCR_OUTPUT "BIN")
message(STATUS "No OCR_OUTPUT, defaulting to BIN")
endif ()
# option(OCR_BENCHMARK "build benchmark" ON)
# set(OCR_BENCHMARK ON)
#set(OCR_OUTPUT "BIN")
if (NOT DEFINED OCR_ONNX)
set(OCR_ONNX "CPU")
message(STATUS "No OCR_ONNX, defaulting to CPU")
endif ()
#set(OCR_OUTPUT "GPU")
# 编译选项和定义: 设置 C++ 标准为 C++11,定义了 Unicode 相关的预处理器宏,
# 以及根据构建类型(Debug 或其他)添加编译器标志。
set(CMAKE_CXX_STANDARD 11)
add_definitions(-DUNICODE -D_UNICODE)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
add_definitions("-Wall -g -O0")
else ()
add_definitions("-Wall")
endif ()
# 依赖项处理:
# * OnnxRuntime: 根据 OCR_ONNX 的值(CPU 或 CUDA),包含不同的 CMake 脚本,并查找 OnnxRuntime 库。
# 如果找到,显示库的路径,否则报错退出。
# * OpenCV: 设置 OpenCV 为静态库,包含对应的 CMake 配置文件,并查找 OpenCV 库。如果找到,显示库的路径,否则报错退出。
# OnnxRuntime
if (OCR_ONNX STREQUAL "CPU")
include(${CMAKE_CURRENT_SOURCE_DIR}/onnxruntime-static/OnnxRuntimeWrapper.cmake)
elseif (OCR_ONNX STREQUAL "CUDA") # CUDA
include(${CMAKE_CURRENT_SOURCE_DIR}/onnxruntime-gpu/OnnxRuntimeWrapper.cmake)
endif ()
find_package(OnnxRuntime REQUIRED)
if (OnnxRuntime_FOUND)
message(STATUS "OnnxRuntime_LIBS: ${OnnxRuntime_LIBS}")
message(STATUS "OnnxRuntime_INCLUDE_DIRS: ${OnnxRuntime_INCLUDE_DIRS}")
else ()
message(FATAL_ERROR "onnxruntime Not Found!")
endif (OnnxRuntime_FOUND)
# OpenCV
set(BUILD_SHARED_LIBS false)
include(${CMAKE_CURRENT_SOURCE_DIR}/opencv-static/OpenCVWrapperConfig.cmake)
find_package(OpenCV REQUIRED)
if (OpenCV_FOUND)
message(STATUS "OpenCV_LIBS: ${OpenCV_LIBS}")
message(STATUS "OpenCV_INCLUDE_DIRS: ${OpenCV_INCLUDE_DIRS}")
else ()
message(FATAL_ERROR "opencv Not Found!")
endif (OpenCV_FOUND)
# 源文件和包含目录: 通过指定源代码目录下的所有 .cpp 文件,并将项目的 include 目录添加到包含目录中。
# project include
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
# source
file(GLOB OCR_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
set(OCR_COMPILE_CODE ${OCR_SRC})
# JNI 支持: 如果输出类型为 JNI,则查找 JNI 库和头文件 ,并将它们添加到项目中。
# 目标定义: 根据输出类型(BIN、CLIB、JNI),添加一个可执行文件或共享库目标,
# 并链接到 OnnxRuntime 和 OpenCV 库。还会根据不同的输出类型定义特定的预处理器宏。
# JNI
if (OCR_OUTPUT STREQUAL "JNI")
find_package(JNI REQUIRED)
if (JNI_FOUND)
message("JNI FOUND")
message(STATUS "JNI_LIBS: ${JNI_LIBS}")
message(STATUS "JNI_INCLUDE_DIRS: ${JNI_INCLUDE_DIRS}")
include_directories(${JNI_INCLUDE_DIRS})
else ()
message(FATAL_ERROR "JNI Not Found!")
endif ()
endif ()
if (OCR_OUTPUT STREQUAL "JNI") # JNI
add_library(RapidOcrOnnx SHARED ${OCR_COMPILE_CODE})
target_compile_definitions(RapidOcrOnnx PRIVATE __JNI__)
target_link_libraries(RapidOcrOnnx ${OnnxRuntime_LIBS} ${OpenCV_LIBS} ${JNI_LIBS})
elseif (OCR_OUTPUT STREQUAL "CLIB") # CLIB
add_library(RapidOcrOnnx SHARED ${OCR_COMPILE_CODE})
target_compile_definitions(RapidOcrOnnx PRIVATE __CLIB__)
target_link_libraries(RapidOcrOnnx ${OnnxRuntime_LIBS} ${OpenCV_LIBS})
elseif (OCR_OUTPUT STREQUAL "BIN") # BIN
add_executable(RapidOcrOnnx ${OCR_COMPILE_CODE})
target_compile_definitions(RapidOcrOnnx PRIVATE __EXEC__)
target_link_libraries(RapidOcrOnnx ${OnnxRuntime_LIBS} ${OpenCV_LIBS})
endif ()
# 安装规则: 定义了如何安装目标和头文件(对于 CLIB 输出类型)。如果输出类型为 CLIB,则安装共享库和头文件。
install(TARGETS RapidOcrOnnx EXPORT RapidOcrOnnx)
if (OCR_OUTPUT STREQUAL "CLIB") # CLIB
file(GLOB OCR_INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
install(FILES ${OCR_INCLUDE} DESTINATION include)
endif ()
# CUDA 支持: 如果使用 CUDA 版本的 ONNX 运行时,为目标添加定义以启用 CUDA 支持。
if (OCR_ONNX STREQUAL "CUDA")
target_compile_definitions(RapidOcrOnnx PRIVATE __CUDA__)
endif ()
# 可选的 Windows CRT 链接设置: 如果指定了 OCR_BUILD_CRT 选项,
# 包含一个额外的 CMake 脚本来处理 CRT(C 运行时库)链接设置。
# Windows Link CRT
if (OCR_BUILD_CRT STREQUAL "True")
include(${CMAKE_CURRENT_SOURCE_DIR}/OcrCRTLinkage.cmake)
endif ()
其它
CMakeSettings.json
在 Visual Studio 中,CMakeSettings.json 是一个配置文件,用于定义和存储特定于项目的设置,这些设置控制如何在 Visual Studio 环境中构建和调试使用 CMake 的项目。
它允许开发者为项目配置多个不同的构建环境,例如不同的构建类型(Debug、Release)、目标平台(x86、x64、ARM)以及其他任意的 CMake 变量。这种方式提供了一种灵活的方法来管理项目的构建过程,而不需要修改 CMakeLists.txt 文件本身。
-
CMakeLists.txt 文件是 CMake 项目的核心,它包含了定义项目所需的所有指令和逻辑,如添加源文件、查找和链接依赖库、设置编译器选项等。CMakeLists.txt 定义了项目的构建规则和依赖关系,是跨平台的,并且通常不包含特定于某个开发环境的配置。
-
CMakeSettings.json 是 Visual Studio 特有的配置文件,用于在 Visual Studio 环境中定制 CMake 构建过程。通过这个 文件,开发者可以为不同的场景和配置指定不同的构建参数,比如不同的编译器选项、环境变量、CMake 变量等。这使得开发者可以在不改变 CMakeLists.txt 的情况下,调整构建过程以适应不同的开发需求。